<?php
namespace Base\UnitTest;

/**
 * Test case which enables overriding of functions with runkit.
 *
 * 1. To override a number of system function do
 *
 *               $mock = $this->runkitMockFunctions(array(...));
 *
 * 2. To define expected behaivour use $mock as an ordinary phpunit mock object.
 *
 * 3. To revert overridden functions back call $this->runkitRevertAll();
 *
 * Example:
 *
 *       class MyCurlTest
 *           extends \Base\UnitTest\RunkitTestCase
 *       {
 *           protected $mock;
 *
 *           protected function setUp()
 *           {
 *               $this->mock = $this->runkitMockFunctions(array(
 *                   'curl_init',
 *                   'curl_close',
 *               ));
 *           }
 *
 *           protected function tearDown()
 *           {
 *               $this->runkitRevertAll();
 *           }
 *
 *           public function testInitClose()
 *           {
 *               $this->mock
 *                   ->expects($this->at(0))
 *                   ->method('curl_init')
 *                   ->with()
 *                   ->will($this->returnValue('my_handle'));
 *
 *               $this->mock
 *                   ->expects($this->at(1))
 *                   ->method('curl_close')
 *                   ->with('my_handle');
 *
 *               $handle = curl_init();
 *               $this->assertEquals('my_handle', $handle);
 *               curl_close($handle);
 *           }
 *       }
 *
 * @package Base\UnitTest
 * @version $id$
 * @author Alexey Karapetov <karapetov@gmail.com>
 */
abstract class RunkitTestCase
    extends \PHPUnit_Framework_TestCase
{
    private static $mockedFunctions = array();

    const BACKUP_SUFFIX = '_runkit_mocker_backup';

    /**
     * Method to call from overridden functions.
     * Calls given mock's method with given arguments.
     *
     * @param string    $method Mock's method to call
     * @param array     $args   Arguments to pass to @link $method
     * @return void
     */
    public static function call($func, array $args)
    {
        return call_user_func_array(array(self::$mockedFunctions[$func], $func), $args);
    }

    /**
     * Mark test skipped if runkit is not enabled
     *
     * @return void
     */
    protected function skipTestIfNoRunkit()
    {
        if (!extension_loaded('runkit'))
        {
            $this->markTestSkipped('Runkit extension is not loaded');
        }
    }

    /**
     * Override given functions with mock
     *
     * @param array $funcList Functions to override
     * @return stdClass Mock object
     */
    protected function runkitMockFunctions(array $funcList)
    {
        $this->skipTestIfNoRunkit();

        $mock = $this->getMock('stdClass', $funcList);

        foreach ($funcList as $func)
        {
            $this->runkitOverride($func, '', 'return ' . __CLASS__ . "::call('{$func}', func_get_args());", $mock);
        }

        return $mock;
    }

    /**
     * Override function
     *
     * @param string    $func
     * @param string    $args
     * @param string    $body
     * @param mixed     $mock Mock object for the function
     * @return void
     */
    protected function runkitOverride($func, $args, $body, $mock = null)
    {
        $this->skipTestIfNoRunkit();

        if (array_key_exists($func, self::$mockedFunctions))
        {
            throw new \RuntimeException("Function '{$func}' is marked as mocked already");
        }
        self::$mockedFunctions[$func] = $mock;
        \runkit_function_copy($func, $func . self::BACKUP_SUFFIX);
        \runkit_function_redefine($func, $args, $body);
    }


    /**
     * Revert previously overridden function
     *
     * @param string $func
     * @return void
     */
    protected function runkitRevert($func)
    {
        $this->skipTestIfNoRunkit();

        if (!array_key_exists($func, self::$mockedFunctions))
        {
            throw new \RuntimeException("Function '{$func}' is not marked as mocked");
        }
        unset(self::$mockedFunctions[$func]);

        \runkit_function_remove($func);
        \runkit_function_copy($func . self::BACKUP_SUFFIX, $func);
        \runkit_function_remove($func . self::BACKUP_SUFFIX);
    }

    /**
     * Revert all previously overridden functions
     *
     * @return void
     */
    protected function runkitRevertAll()
    {
        foreach (array_keys(self::$mockedFunctions) as $func)
        {
            $this->runkitRevert($func);
        }
    }
}
